"use strict";
import { inspect } from "util";
// const { inspect } = require("util");
/*
 * vendored in order to fix its dependence on the window global [cds 2020/08/04]
 * otherwise unchanged from https://github.com/jarek-foksa/geometry-polyfill/tree/f36bbc8f4bc43539d980687904ce46c8e915543d
 */

// @info
//   DOMPoint polyfill
// @src
//   https://drafts.fxtf.org/geometry/#DOMPoint
//   https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_point_read_only.cc
class DOMPoint {
  constructor(x = 0, y = 0, z = 0, w = 1) {
    this.x = x;
    this.y = y;
    this.z = z;
    this.w = w;
  }

  static fromPoint(otherPoint) {
    return new DOMPoint(
      otherPoint.x,
      otherPoint.y,
      otherPoint.z !== undefined ? otherPoint.z : 0,
      otherPoint.w !== undefined ? otherPoint.w : 1
    );
  }

  matrixTransform(matrix) {
    if (
      (matrix.is2D || matrix instanceof SVGMatrix) &&
      this.z === 0 &&
      this.w === 1
    ) {
      return new DOMPoint(
        this.x * matrix.a + this.y * matrix.c + matrix.e,
        this.x * matrix.b + this.y * matrix.d + matrix.f,
        0,
        1
      );
    } else {
      return new DOMPoint(
        this.x * matrix.m11 +
          this.y * matrix.m21 +
          this.z * matrix.m31 +
          this.w * matrix.m41,
        this.x * matrix.m12 +
          this.y * matrix.m22 +
          this.z * matrix.m32 +
          this.w * matrix.m42,
        this.x * matrix.m13 +
          this.y * matrix.m23 +
          this.z * matrix.m33 +
          this.w * matrix.m43,
        this.x * matrix.m14 +
          this.y * matrix.m24 +
          this.z * matrix.m34 +
          this.w * matrix.m44
      );
    }
  }

  toJSON() {
    return {
      x: this.x,
      y: this.y,
      z: this.z,
      w: this.w
    };
  }
}

// @info
//   DOMRect polyfill
// @src
//   https://drafts.fxtf.org/geometry/#DOMRect
//   https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_rect_read_only.cc

class DOMRect {
  constructor(x = 0, y = 0, width = 0, height = 0) {
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
  }

  static fromRect(otherRect) {
    return new DOMRect(
      otherRect.x,
      otherRect.y,
      otherRect.width,
      otherRect.height
    );
  }

  get top() {
    return this.y;
  }

  get left() {
    return this.x;
  }

  get right() {
    return this.x + this.width;
  }

  get bottom() {
    return this.y + this.height;
  }

  toJSON() {
    return {
      x: this.x,
      y: this.y,
      width: this.width,
      height: this.height,
      top: this.top,
      left: this.left,
      right: this.right,
      bottom: this.bottom
    };
  }
}

for (let propertyName of ["top", "right", "bottom", "left"]) {
  let propertyDescriptor = Object.getOwnPropertyDescriptor(
    DOMRect.prototype,
    propertyName
  );
  propertyDescriptor.enumerable = true;
  Object.defineProperty(DOMRect.prototype, propertyName, propertyDescriptor);
}

// @info
//   DOMMatrix polyfill (SVG 2)
// @src
//   https://github.com/chromium/chromium/blob/master/third_party/blink/renderer/core/geometry/dom_matrix_read_only.cc
//   https://github.com/tocharomera/generativecanvas/blob/master/node-canvas/lib/DOMMatrix.js

const M11 = 0,
  M12 = 1,
  M13 = 2,
  M14 = 3;
const M21 = 4,
  M22 = 5,
  M23 = 6,
  M24 = 7;
const M31 = 8,
  M32 = 9,
  M33 = 10,
  M34 = 11;
const M41 = 12,
  M42 = 13,
  M43 = 14,
  M44 = 15;

const A = M11,
  B = M12;
const C = M21,
  D = M22;
const E = M41,
  F = M42;

const DEGREE_PER_RAD = 180 / Math.PI;
const RAD_PER_DEGREE = Math.PI / 180;

const $values = Symbol();
const $is2D = Symbol();

let parseMatrix = init => {
  let parsed = init.replace(/matrix\(/, "");
  parsed = parsed.split(/,/, 7);

  if (parsed.length !== 6) {
    throw new Error(`Failed to parse ${init}`);
  }

  parsed = parsed.map(parseFloat);

  return [
    parsed[0],
    parsed[1],
    0,
    0,
    parsed[2],
    parsed[3],
    0,
    0,
    0,
    0,
    1,
    0,
    parsed[4],
    parsed[5],
    0,
    1
  ];
};

let parseMatrix3d = init => {
  let parsed = init.replace(/matrix3d\(/, "");
  parsed = parsed.split(/,/, 17);

  if (parsed.length !== 16) {
    throw new Error(`Failed to parse ${init}`);
  }

  return parsed.map(parseFloat);
};

let parseTransform = tform => {
  let type = tform.split(/\(/, 1)[0];

  if (type === "matrix") {
    return parseMatrix(tform);
  } else if (type === "matrix3d") {
    return parseMatrix3d(tform);
  } else {
    throw new Error(`${type} parsing not implemented`);
  }
};

let setNumber2D = (receiver, index, value) => {
  if (typeof value !== "number") {
    throw new TypeError("Expected number");
  }

  receiver[$values][index] = value;
};

let setNumber3D = (receiver, index, value) => {
  if (typeof value !== "number") {
    throw new TypeError("Expected number");
  }

  if (index === M33 || index === M44) {
    if (value !== 1) {
      receiver[$is2D] = false;
    }
  } else if (value !== 0) {
    receiver[$is2D] = false;
  }

  receiver[$values][index] = value;
};

let newInstance = values => {
  let instance = Object.create(DOMMatrix.prototype);
  instance.constructor = DOMMatrix;
  instance[$is2D] = true;
  instance[$values] = values;

  return instance;
};

let multiply = (first, second) => {
  let dest = new Float64Array(16);

  for (let i = 0; i < 4; i++) {
    for (let j = 0; j < 4; j++) {
      let sum = 0;

      for (let k = 0; k < 4; k++) {
        sum += first[i * 4 + k] * second[k * 4 + j];
      }

      dest[i * 4 + j] = sum;
    }
  }

  return dest;
};

class DOMMatrix {
  get m11() {
    return this[$values][M11];
  }
  set m11(value) {
    setNumber2D(this, M11, value);
  }
  get m12() {
    return this[$values][M12];
  }
  set m12(value) {
    setNumber2D(this, M12, value);
  }
  get m13() {
    return this[$values][M13];
  }
  set m13(value) {
    setNumber3D(this, M13, value);
  }
  get m14() {
    return this[$values][M14];
  }
  set m14(value) {
    setNumber3D(this, M14, value);
  }
  get m21() {
    return this[$values][M21];
  }
  set m21(value) {
    setNumber2D(this, M21, value);
  }
  get m22() {
    return this[$values][M22];
  }
  set m22(value) {
    setNumber2D(this, M22, value);
  }
  get m23() {
    return this[$values][M23];
  }
  set m23(value) {
    setNumber3D(this, M23, value);
  }
  get m24() {
    return this[$values][M24];
  }
  set m24(value) {
    setNumber3D(this, M24, value);
  }
  get m31() {
    return this[$values][M31];
  }
  set m31(value) {
    setNumber3D(this, M31, value);
  }
  get m32() {
    return this[$values][M32];
  }
  set m32(value) {
    setNumber3D(this, M32, value);
  }
  get m33() {
    return this[$values][M33];
  }
  set m33(value) {
    setNumber3D(this, M33, value);
  }
  get m34() {
    return this[$values][M34];
  }
  set m34(value) {
    setNumber3D(this, M34, value);
  }
  get m41() {
    return this[$values][M41];
  }
  set m41(value) {
    setNumber2D(this, M41, value);
  }
  get m42() {
    return this[$values][M42];
  }
  set m42(value) {
    setNumber2D(this, M42, value);
  }
  get m43() {
    return this[$values][M43];
  }
  set m43(value) {
    setNumber3D(this, M43, value);
  }
  get m44() {
    return this[$values][M44];
  }
  set m44(value) {
    setNumber3D(this, M44, value);
  }

  get a() {
    return this[$values][A];
  }
  set a(value) {
    setNumber2D(this, A, value);
  }
  get b() {
    return this[$values][B];
  }
  set b(value) {
    setNumber2D(this, B, value);
  }
  get c() {
    return this[$values][C];
  }
  set c(value) {
    setNumber2D(this, C, value);
  }
  get d() {
    return this[$values][D];
  }
  set d(value) {
    setNumber2D(this, D, value);
  }
  get e() {
    return this[$values][E];
  }
  set e(value) {
    setNumber2D(this, E, value);
  }
  get f() {
    return this[$values][F];
  }
  set f(value) {
    setNumber2D(this, F, value);
  }

  get is2D() {
    return this[$is2D];
  }

  get isIdentity() {
    let values = this[$values];

    return (
      values[M11] === 1 &&
      values[M12] === 0 &&
      values[M13] === 0 &&
      values[M14] === 0 &&
      values[M21] === 0 &&
      values[M22] === 1 &&
      values[M23] === 0 &&
      values[M24] === 0 &&
      values[M31] === 0 &&
      values[M32] === 0 &&
      values[M33] === 1 &&
      values[M34] === 0 &&
      values[M41] === 0 &&
      values[M42] === 0 &&
      values[M43] === 0 &&
      values[M44] === 1
    );
  }

  static fromMatrix(init) {
    if (init instanceof DOMMatrix) {
      return new DOMMatrix(init[$values]);
    } else if (init instanceof SVGMatrix) {
      return new DOMMatrix([init.a, init.b, init.c, init.d, init.e, init.f]);
    } else {
      throw new TypeError("Expected DOMMatrix");
    }
  }

  static fromFloat32Array(init) {
    if (!(init instanceof Float32Array))
      throw new TypeError("Expected Float32Array");
    return new DOMMatrix(init);
  }

  static fromFloat64Array(init) {
    if (!(init instanceof Float64Array))
      throw new TypeError("Expected Float64Array");
    return new DOMMatrix(init);
  }

  // @type
  // (Float64Array) => void
  constructor(init) {
    this[$is2D] = true;

    this[$values] = new Float64Array([
      1,
      0,
      0,
      0,
      0,
      1,
      0,
      0,
      0,
      0,
      1,
      0,
      0,
      0,
      0,
      1
    ]);

    // Parse CSS transformList
    if (typeof init === "string") {
      if (init === "") {
        return;
      } else {
        let tforms = init.split(/\)\s+/, 20).map(parseTransform);

        if (tforms.length === 0) {
          return;
        }

        init = tforms[0];

        for (let i = 1; i < tforms.length; i++) {
          init = multiply(tforms[i], init);
        }
      }
    }

    let i = 0;

    if (init && init.length === 6) {
      setNumber2D(this, A, init[i++]);
      setNumber2D(this, B, init[i++]);
      setNumber2D(this, C, init[i++]);
      setNumber2D(this, D, init[i++]);
      setNumber2D(this, E, init[i++]);
      setNumber2D(this, F, init[i++]);
    } else if (init && init.length === 16) {
      setNumber2D(this, M11, init[i++]);
      setNumber2D(this, M12, init[i++]);
      setNumber3D(this, M13, init[i++]);
      setNumber3D(this, M14, init[i++]);
      setNumber2D(this, M21, init[i++]);
      setNumber2D(this, M22, init[i++]);
      setNumber3D(this, M23, init[i++]);
      setNumber3D(this, M24, init[i++]);
      setNumber3D(this, M31, init[i++]);
      setNumber3D(this, M32, init[i++]);
      setNumber3D(this, M33, init[i++]);
      setNumber3D(this, M34, init[i++]);
      setNumber2D(this, M41, init[i++]);
      setNumber2D(this, M42, init[i++]);
      setNumber3D(this, M43, init[i++]);
      setNumber3D(this, M44, init[i]);
    } else if (init !== undefined) {
      throw new TypeError("Expected string or array.");
    }
  }

  dump() {
    let mat = this[$values];
    console.log([
      mat.slice(0, 4),
      mat.slice(4, 8),
      mat.slice(8, 12),
      mat.slice(12, 16)
    ]);
  }

  [inspect.custom](depth, options) {
    if (depth < 0) return "[DOMMatrix]";

    let { a, b, c, d, e, f, is2D, isIdentity } = this;
    if (this.is2D) {
      return `DOMMatrix ${inspect(
        { a, b, c, d, e, f, is2D, isIdentity },
        { colors: true }
      )}`;
    } else {
      let {
        m11,
        m12,
        m13,
        m14,
        m21,
        m22,
        m23,
        m24,
        m31,
        m32,
        m33,
        m34,
        m41,
        m42,
        m43,
        m44,
        is2D,
        isIdentity
      } = this;
      return `DOMMatrix ${inspect(
        {
          a,
          b,
          c,
          d,
          e,
          f,
          m11,
          m12,
          m13,
          m14,
          m21,
          m22,
          m23,
          m24,
          m31,
          m32,
          m33,
          m34,
          m41,
          m42,
          m43,
          m44,
          is2D,
          isIdentity
        },
        { colors: true }
      )}`;
    }
  }

  multiply(other) {
    return newInstance(this[$values]).multiplySelf(other);
  }

  multiplySelf(other) {
    this[$values] = multiply(other[$values], this[$values]);

    if (!other.is2D) {
      this[$is2D] = false;
    }

    return this;
  }

  preMultiplySelf(other) {
    this[$values] = multiply(this[$values], other[$values]);

    if (!other.is2D) {
      this[$is2D] = false;
    }

    return this;
  }

  translate(tx, ty, tz) {
    return newInstance(this[$values]).translateSelf(tx, ty, tz);
  }

  translateSelf(tx = 0, ty = 0, tz = 0) {
    this[$values] = multiply(
      [1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, tx, ty, tz, 1],
      this[$values]
    );

    if (tz !== 0) {
      this[$is2D] = false;
    }

    return this;
  }

  scale(scaleX, scaleY, scaleZ, originX, originY, originZ) {
    return newInstance(this[$values]).scaleSelf(
      scaleX,
      scaleY,
      scaleZ,
      originX,
      originY,
      originZ
    );
  }

  scale3d(scale, originX, originY, originZ) {
    return newInstance(this[$values]).scale3dSelf(
      scale,
      originX,
      originY,
      originZ
    );
  }

  scale3dSelf(scale, originX, originY, originZ) {
    return this.scaleSelf(scale, scale, scale, originX, originY, originZ);
  }

  scaleSelf(scaleX, scaleY, scaleZ, originX, originY, originZ) {
    // Not redundant with translate's checks because we need to negate the values later.
    if (typeof originX !== "number") originX = 0;
    if (typeof originY !== "number") originY = 0;
    if (typeof originZ !== "number") originZ = 0;

    this.translateSelf(originX, originY, originZ);

    if (typeof scaleX !== "number") scaleX = 1;
    if (typeof scaleY !== "number") scaleY = scaleX;
    if (typeof scaleZ !== "number") scaleZ = 1;

    this[$values] = multiply(
      [scaleX, 0, 0, 0, 0, scaleY, 0, 0, 0, 0, scaleZ, 0, 0, 0, 0, 1],
      this[$values]
    );

    this.translateSelf(-originX, -originY, -originZ);

    if (scaleZ !== 1 || originZ !== 0) {
      this[$is2D] = false;
    }

    return this;
  }

  rotateFromVector(x, y) {
    return newInstance(this[$values]).rotateFromVectorSelf(x, y);
  }

  rotateFromVectorSelf(x = 0, y = 0) {
    let theta = x === 0 && y === 0 ? 0 : Math.atan2(y, x) * DEGREE_PER_RAD;
    return this.rotateSelf(theta);
  }

  rotate(rotX, rotY, rotZ) {
    return newInstance(this[$values]).rotateSelf(rotX, rotY, rotZ);
  }

  rotateSelf(rotX, rotY, rotZ) {
    if (rotY === undefined && rotZ === undefined) {
      rotZ = rotX;
      rotX = rotY = 0;
    }

    if (typeof rotY !== "number") rotY = 0;
    if (typeof rotZ !== "number") rotZ = 0;

    if (rotX !== 0 || rotY !== 0) {
      this[$is2D] = false;
    }

    rotX *= RAD_PER_DEGREE;
    rotY *= RAD_PER_DEGREE;
    rotZ *= RAD_PER_DEGREE;

    let c = Math.cos(rotZ);
    let s = Math.sin(rotZ);

    this[$values] = multiply(
      [c, s, 0, 0, -s, c, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
      this[$values]
    );

    c = Math.cos(rotY);
    s = Math.sin(rotY);

    this[$values] = multiply(
      [c, 0, -s, 0, 0, 1, 0, 0, s, 0, c, 0, 0, 0, 0, 1],
      this[$values]
    );

    c = Math.cos(rotX);
    s = Math.sin(rotX);

    this[$values] = multiply(
      [1, 0, 0, 0, 0, c, s, 0, 0, -s, c, 0, 0, 0, 0, 1],
      this[$values]
    );

    return this;
  }

  rotateAxisAngle(x, y, z, angle) {
    return newInstance(this[$values]).rotateAxisAngleSelf(x, y, z, angle);
  }

  rotateAxisAngleSelf(x = 0, y = 0, z = 0, angle = 0) {
    let length = Math.sqrt(x * x + y * y + z * z);

    if (length === 0) {
      return this;
    }

    if (length !== 1) {
      x /= length;
      y /= length;
      z /= length;
    }

    angle *= RAD_PER_DEGREE;

    let c = Math.cos(angle);
    let s = Math.sin(angle);
    let t = 1 - c;
    let tx = t * x;
    let ty = t * y;

    this[$values] = multiply(
      [
        tx * x + c,
        tx * y + s * z,
        tx * z - s * y,
        0,
        tx * y - s * z,
        ty * y + c,
        ty * z + s * x,
        0,
        tx * z + s * y,
        ty * z - s * x,
        t * z * z + c,
        0,
        0,
        0,
        0,
        1
      ],
      this[$values]
    );

    if (x !== 0 || y !== 0) {
      this[$is2D] = false;
    }

    return this;
  }

  skewX(sx) {
    return newInstance(this[$values]).skewXSelf(sx);
  }

  skewXSelf(sx) {
    if (typeof sx !== "number") {
      return this;
    }

    let t = Math.tan(sx * RAD_PER_DEGREE);

    this[$values] = multiply(
      [1, 0, 0, 0, t, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
      this[$values]
    );

    return this;
  }

  skewY(sy) {
    return newInstance(this[$values]).skewYSelf(sy);
  }

  skewYSelf(sy) {
    if (typeof sy !== "number") {
      return this;
    }

    let t = Math.tan(sy * RAD_PER_DEGREE);

    this[$values] = multiply(
      [1, t, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1],
      this[$values]
    );

    return this;
  }

  flipX() {
    return newInstance(
      multiply([-1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[$values])
    );
  }

  flipY() {
    return newInstance(
      multiply([1, 0, 0, 0, 0, -1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 1], this[$values])
    );
  }

  inverse() {
    return newInstance(this[$values]).invertSelf();
  }

  invertSelf() {
    if (this[$is2D]) {
      let det =
        this[$values][A] * this[$values][D] -
        this[$values][B] * this[$values][C];

      // Invertable
      if (det !== 0) {
        let result = new DOMMatrix();

        result.a = this[$values][D] / det;
        result.b = -this[$values][B] / det;
        result.c = -this[$values][C] / det;
        result.d = this[$values][A] / det;
        result.e =
          (this[$values][C] * this[$values][F] -
            this[$values][D] * this[$values][E]) /
          det;
        result.f =
          (this[$values][B] * this[$values][E] -
            this[$values][A] * this[$values][F]) /
          det;

        return result;
      }

      // Not invertable
      else {
        this[$is2D] = false;

        this[$values] = [
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN,
          NaN
        ];
      }
    } else {
      throw new Error("3D matrix inversion is not implemented.");
    }
  }

  setMatrixValue(transformList) {
    let temp = new DOMMatrix(transformList);

    this[$values] = temp[$values];
    this[$is2D] = temp[$is2D];

    return this;
  }

  transformPoint(point) {
    let x = point.x;
    let y = point.y;
    let z = point.z;
    let w = point.w;

    let values = this[$values];

    let nx =
      values[M11] * x + values[M21] * y + values[M31] * z + values[M41] * w;
    let ny =
      values[M12] * x + values[M22] * y + values[M32] * z + values[M42] * w;
    let nz =
      values[M13] * x + values[M23] * y + values[M33] * z + values[M43] * w;
    let nw =
      values[M14] * x + values[M24] * y + values[M34] * z + values[M44] * w;

    return new DOMPoint(nx, ny, nz, nw);
  }

  toFloat32Array() {
    return Float32Array.from(this[$values]);
  }

  toFloat64Array() {
    return this[$values].slice(0);
  }

  toJSON() {
    return {
      a: this.a,
      b: this.b,
      c: this.c,
      d: this.d,
      e: this.e,
      f: this.f,
      m11: this.m11,
      m12: this.m12,
      m13: this.m13,
      m14: this.m14,
      m21: this.m21,
      m22: this.m22,
      m23: this.m23,
      m24: this.m24,
      m31: this.m31,
      m32: this.m32,
      m33: this.m33,
      m34: this.m34,
      m41: this.m41,
      m42: this.m42,
      m43: this.m43,
      m44: this.m44,
      is2D: this.is2D,
      isIdentity: this.isIdentity
    };
  }

  toString() {
    if (this.is2D) {
      return `matrix(${this.a}, ${this.b}, ${this.c}, ${this.d}, ${this.e}, ${this.f})`;
    } else {
      return `matrix3d(${this[$values].join(", ")})`;
    }
  }
}

for (let propertyName of [
  "a",
  "b",
  "c",
  "d",
  "e",
  "f",
  "m11",
  "m12",
  "m13",
  "m14",
  "m21",
  "m22",
  "m23",
  "m24",
  "m31",
  "m32",
  "m33",
  "m34",
  "m41",
  "m42",
  "m43",
  "m44",
  "is2D",
  "isIdentity"
]) {
  let propertyDescriptor = Object.getOwnPropertyDescriptor(
    DOMMatrix.prototype,
    propertyName
  );
  propertyDescriptor.enumerable = true;
  Object.defineProperty(DOMMatrix.prototype, propertyName, propertyDescriptor);
}

// module.exports = {DOMPoint, DOMMatrix, DOMRect}

const obj = { DOMPoint, DOMMatrix, DOMRect };
export default obj;
